package org.gitman.agentplatform.core;

import java.io.File;
import java.io.PrintWriter;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
import java.util.Stack;

import org.gitman.agentplatform.agent.api.Agent;
import org.gitman.agentplatform.agent.api.AgentInfo;
import org.gitman.agentplatform.agent.api.AgentPlan;
import org.gitman.agentplatform.agent.api.AgentState;
import org.gitman.agentplatform.agent.api.InternalAgentInfo;
import org.gitman.agentplatform.core.exceptions.BadArgsException;
import org.gitman.agentplatform.core.exceptions.NoGraphException;
import org.gitman.agentplatform.model.Graph;
import org.gitman.agentplatform.model.Vertex;
import org.gitman.agentplatform.model.utils.GraphTools;
import org.gitman.agentplatform.ui.api.View;
import org.gitman.agentplatform.ui.api.View.MessageType;

/**
 * This is class representing simulation process. 
 * A new simulation in AgentPlatform will have new simulation class.
 * 
 * @author gitman
 *
 */
public class Simulation {
	private AgentManager agentManager;
	private PlatformManager platformManager;
	private boolean initialized;
	private Graph graph;
	private SimulationThread simulationThread;
	private View view;
	private int iteration;
	private Random rand;
	private SimulationStatistic simulationStatistic;
	private Map<Integer, AgentStatistic> id2agentStatistic;
	private MultirunThread multirunThread;
	
	public Simulation(PlatformManager platformManager){
		this.platformManager = platformManager;
		agentManager = new AgentManager(platformManager);
		initialized = false;
		rand = new Random(Calendar.getInstance().getTimeInMillis());
		view = platformManager.getView();
	}	
	public boolean init() throws NoGraphException{
		if(!initialized){
			agentManager.initializeAgentQueue(graph);
			iteration = 0;			
			simulationStatistic = new SimulationStatistic();
			view = platformManager.getView();
			view.init(agentManager.getAllAgentsInfo());
			id2agentStatistic = new HashMap<Integer, AgentStatistic>(agentManager.getAllAgentsInfo().size());
			for(AgentInfo info : agentManager.getAllAgentsInfo()){
				AgentStatistic agentStatistic = new AgentStatistic();
				id2agentStatistic.put(info.getId(), agentStatistic);
				Stack<Integer> shorthestPath = GraphTools.getShorthestPath(graph, info.getStartVertexID(), info.getEndVertexID());
				if(shorthestPath == null){
					View.showMessage("Simulation can not be initialized.\nCould not find any path for agent with ID=" + info.getId(), MessageType.ERROR);
					return false;
				}else{
					agentStatistic.minPath = shorthestPath.size();
				}
			}
			initialized = true;	
		}		
		return initialized;
	}
	public void doStep() throws NoGraphException{
		if(init()){
			if(iteration < PlatformManager.MAX_ITERATIONS){
				Agent agent = agentManager.nextAgent();
				if(agent != null){
					InternalAgentInfo info = agentManager.getAgentInfo(agent);
					AgentStatistic statistic = id2agentStatistic.get(info.getId());
					Integer observ = null;
					if(agentManager.isUseDefObserv()){
						observ = getNextRandom(agentManager.getDefaultMinObserv(), agentManager.getDefaultMaxObserv());
					}else{
						observ = getNextRandom(info.getMinObserv(), info.getMaxObserv());
					}
					Collection<AgentInfo> agentsInRange = graph.getAgentsInRange(info, agentManager.getQueuedAgentsInfo(), observ);
					agentsInRange.remove(info);
					int it = info.getIteration();
					if(it == iteration){
						iteration++;
						view.updateIteration(iteration);
					}
					info.setIteration(iteration);
					statistic.time -= System.nanoTime();
					Integer moveID = agent.getStep(agentsInRange);
					statistic.time += System.nanoTime();
					statistic.path = info.getIteration();
					if(moveID == null  ){
						agentManager.addAgent2Queue(agent);
						view.error(info + "agent does not want to move, null ID returned");
					}else if(moveID.equals(info.getCurrentVertexID())){
						agentManager.addAgent2Queue(agent);
						view.warn(info + "agent does not want to move, returned its current vertex ID");
					}else{
						Integer prevID = info.getCurrentVertexID();
						boolean a = canMove(prevID, moveID);
						double relie = info.getInternalRelie();
						if(agentManager.isUseDefReliab()){
							relie = agentManager.getDefaultReliab();
						}
						boolean b = relie >= rand.nextDouble();
						if(!b){
							statistic.failed++;
						}
						if( a&&b ){
							info.setCurrentVertexID(moveID);
							view.viewMove(info, prevID);
							view.log(info + "agent moved from " + prevID + " to " + moveID);
						}else{
							view.warn(info + "agent can not move:" + (a? " failure (agents reliablity:"+ info.getInternalRelie()+")" : " destination (" + moveID + ") occupied"));
						}						
						if(!info.isTargetAchived()){
							agentManager.addAgent2Queue(agent);					
						}else{
							info.setAgentState(AgentState.done);							
							view.update();
						}
					}
					Integer predict = null;
					if(agentManager.isUseDefPredict()){
						predict = getNextRandom(agentManager.getDefaultMinPredict(), agentManager.getDefaultMaxPredict());
					}else{
						predict = getNextRandom(info.getMinPredict(), info.getMaxPredict());
					}
					agentsInRange = graph.getAgentsInRange(info, agentManager.getQueuedAgentsInfo(), observ);
					agentsInRange.remove(info);
					statistic.time -= System.nanoTime();
					AgentPlan plan = agent.getPlan(predict,agentsInRange);
					statistic.time += System.nanoTime();
					if(plan == null){
						view.error(info + "agent does not have a plan!");
					}else if (!verifyPlan(plan, info, predict)){
						view.warn(info + "agent returned wrong plan");
					}
					info.setAgentPlan(plan);
				}else{
					finishSimulation();				
				}
			}else{
				finishSimulation();
			}
		}
	}
	private void finishSimulation() {
		view.log("simulation is done");
		pause();
		view.openSummary();
	}
	private boolean canMove(Integer curr, Integer moveID){
		for(InternalAgentInfo info : agentManager.getQueuedAgentsInfo()){
			if(info.getCurrentVertexID().equals(moveID)){
				return false;
			}
		}
		return graph.canMove(curr, moveID);
	}
	private boolean verifyPlan(AgentPlan plan, InternalAgentInfo info, int predict){
		for(Integer[] path : plan.getPlans().keySet()){
			if(path.length > predict){
				// to long path was returned
				return false;
			}else if(path.length < predict && (path.length == 0 || !path[path.length-1].equals(info.getEndVertexID()))){
				// path is to short and does not end with end vertex
				return false;
			}
		}		
		return true;
	}
	public AgentManager getAgentManager(){
		return agentManager;
	}
	public void pause() {
		if(simulationThread != null){
			simulationThread.setRunning(false);
		}
	}
	public void restart() throws NoGraphException {
		pause();
		initialized = false;
		init();
		simulationThread = null;
	}
	public void fforward() {
		if(simulationThread == null){
			simulationThread = new SimulationThread();			
			simulationThread.start();
		}
		simulationThread.setfforward();
		simulationThread.setRunning(true);
	}
	public void play(){
		if(simulationThread == null){
			simulationThread = new SimulationThread();			
			simulationThread.start();
		}
		simulationThread.setPlay();
		simulationThread.setRunning(true);
	}
	public Graph getGraph() {
		return graph;
	}
	public void setGraph(Graph graph) {
		this.graph = graph;
	}
	public boolean verifyAgentInfo(InternalAgentInfo info) {
		view = platformManager.getView();
		boolean isOk = true;
		view.log("### Agents Veryfication ###");
		if(graph == null){
			view.error("no graph in editor");
			isOk = false;
		}else{
			if(graph.getVertexByID(info.getStartVertexID()) == null){				
				view.error("wrong start vertex: " + info.getStartVertexID());
				isOk = false;
			}else{
				Integer id = info.getStartVertexID();
				for(InternalAgentInfo ai : agentManager.getAllAgentsInfo()){
					if(!ai.getId().equals(info.getId()) && id.equals(ai.getStartVertexID())){
						isOk = false;
						view.error("There is already agent [id=" + ai.getId() + "] with this start vertex: " + id);
					}
				}				
			}
			if(graph.getVertexByID(info.getEndVertexID()) == null){
				view.error("wrong end vertex: " + info.getEndVertexID());
				isOk = false;
			}
		}
		if(info.getMinObserv() < 0){
			view.error("wrong observability parameter: " + info.getMinObserv());
			isOk = false;
		}
		if(info.getMinPredict() < 0){
			view.error( "wrong predictability parameter: " + info.getMinPredict());
			isOk = false;
		}
		if(info.getInternalRelie() < 0.0 || info.getInternalRelie() > 1.0){
			view.error("wrong relieability parameter: " + info.getInternalRelie());
			isOk = false;
		}
		
		if(!isOk){
			View.showMessage("Can not create agent. Check Logging console for details.", MessageType.ERROR);
		}
		
		return isOk;
	}
	public Integer getNextRandom(Integer min, Integer max){
		return rand.nextInt(max-min+1) + min;
	}
	public AgentStatistic getAgentStatistic(Integer id) {
		if(id2agentStatistic == null){
			return new AgentStatistic();
		}
		return id2agentStatistic.get(id);
	}	
	public int getIteration() {
		return iteration;
	}
	public SimulationStatistic getStatistic(){
		simulationStatistic.state = "complete";
		for(AgentInfo info : agentManager.getAllAgentsInfo()){
			if(!info.getAgentState().equals(AgentState.done)){
				simulationStatistic.state = "incomplete";
			}
		}
		simulationStatistic.iterations = iteration;
		simulationStatistic.agents = agentManager.getAllAgentsInfo().size();
		simulationStatistic.vertices = graph.getVertices().size();
		simulationStatistic.edges = 0;
		for(Vertex v : graph.getVertices()){
			simulationStatistic.edges += v.getNeighbours().size();
		}
		simulationStatistic.time = 0.0;
		simulationStatistic.minIterations = 0;
		double sum = 0;
		for(AgentStatistic statistic  : id2agentStatistic.values()){
			simulationStatistic.time += statistic.time/1000000.0;
			if(statistic.minPath > simulationStatistic.minIterations){
				simulationStatistic.minIterations = statistic.minPath;
			}
			sum += Math.pow(statistic.path - statistic.minPath, 2);
		}
		simulationStatistic.rating = sum / simulationStatistic.agents;
		return simulationStatistic;
	}
	public void multirun(MultiRunParameters params) throws NoGraphException{
		if(graph == null){
			throw new NoGraphException();
		}
		int runTime = params.computeRunTime();		
		view.log("Multirun mode, simulation will run: " + runTime + " times");
		view.initMultiRun(runTime);		
		multirunThread = new MultirunThread(params);
		multirunThread.start();		
	}
	public void verifyMultiRunParams(MultiRunParameters params) throws Exception{
		if(params.isChangeObserv() 
				&& (params.getMinObserv() >= params.getMaxObserv() 
						|| params.getStepObserv() <= 0 
						|| params.getMinObserv() < 0)){
			throw new IllegalArgumentException("Wrong observability multirun confgiuration");
		}
		if(params.isChangePredict() 
				&& (params.getMinPredict() >= params.getMaxPredict() 
						|| params.getStepPredict() <= 0 
						|| params.getMinPredict() < 0)){
			throw new IllegalArgumentException("Wrong predictability multirun confgiuration");
		}
		if(params.isChangeRelie() 
				&& (params.getMinRelie() >= params.getMaxRelie() 
						|| params.getStepRelie() <= 0.0 
						|| params.getMinRelie() <= 0.0 
						|| params.getMaxRelie() > 1.0)){
			throw new IllegalArgumentException("Wrong relieability multirun confgiuration");
		}
		if(params.isRandOrder() && params.getRandOrderNr()<=0){
			throw new IllegalArgumentException("Wrong random order multirun confgiuration");
		}
		if(!params.isChangeObserv() && !params.isChangePredict()
				&& !params.isChangeRelie() && !params.isRandOrder()){
			throw new IllegalArgumentException("No multi run parameter configured");
		}
	}
	public void setView(View view) {
		this.view = view;
	}
	public void saveMultirunStatistics(File file) throws Exception{
		if(multirunThread != null){
			LinkedList<SimulationStatistic> statistics = multirunThread.getStatistics();
			MultiRunParameters params = multirunThread.getMultiRunParameters();
			
			int changing = 0;
			if(params.isChangeObserv()){
				changing++;
			}
			if(params.isChangePredict()){
				changing++;
			}
			if(params.isChangeRelie()){
				changing ++;
			}
			if(changing != 2){ //for the 3D plot
				throw new BadArgsException("Can not genereate 3D plot. Exactly 2 parametrs must be changing.");
			}
			PrintWriter out = new PrintWriter(file);
			int idx = 0;
			
			double inf = 1.0;
			for(SimulationStatistic s: statistics){
				if(s.state.equals("complete") && s.rating > inf){
					inf = s.rating;
				}
			}
			inf *= 2;
			
			for(Integer o : params.getObservQueue()){
				for(Integer p : params.getPredictQueue()){
					for(Double r :params.getRelieQueue()){
						
						double avg = 0.0;						
						for(int i =0;i<params.getRandOrderNr();i++){
							SimulationStatistic ss = statistics.get(idx);
							double rating = inf;
							if(ss.state.equals("complete")){
								rating = ss.rating;
							}
							avg += rating;
							idx ++;
						}
						avg /= params.getRandOrderNr();
						out.println((o != null? o.toString() + " " : "" ) + (p != null? p.toString() + " " : "" ) + (r != null? r.toString() + " " : "" ) + avg );	
												
					}
					if(o == null){
						out.println("");
					}
				}
				if(o != null){
					out.println("");
				}
			}
			
		
			out.close();
		}
	}
	
	private class SimulationThread extends Thread{
		public static final int SLEEP_DELY = 100;
		private boolean isRunning = false;
		private boolean isFForward = false;
		public void setRunning(boolean isRunning) {
			this.isRunning = isRunning;
		}
		public void setfforward() {
			isFForward = true;
		}
		public void setPlay() {
			isFForward = false;
		}
		@Override
		public void run() {
			try{			
				if(init()){
					while(!agentManager.isAgentQueueEmpty()){
						while(!isRunning){
							try {
								Thread.sleep(SLEEP_DELY);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
						doStep();
						if(!isFForward){
							try {
								Thread.sleep(PlatformManager.PALY_DELY);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
						
					}
					finishSimulation();
				}
			}catch (NoGraphException e) {
				View.showMessage(e.getMessage(), MessageType.ERROR);
			}
		}
	}
	private class MultirunThread extends Thread{
		private MultiRunParameters params;	
		private Double defaultReliab;
		private Integer defaultMinObserv;
		private Integer defaultMinPredict;
		private Integer defaultMaxObserv;
		private Integer defaultMaxPredict;
		private boolean useDefReliab;
		private boolean useDefObserv;
		private boolean useDefPredict;
		private LinkedList<SimulationStatistic> statistics;
		
		public MultirunThread(MultiRunParameters params){
			this.params = params;
		}
				
		@Override
		public void run() {			
			backupParams();	
			statistics = new LinkedList<SimulationStatistic>();	
			int nr=0;
			id2agentStatistic = new HashMap<Integer, AgentStatistic>(agentManager.getAllAgentsInfo().size());
			for(AgentInfo info : agentManager.getAllAgentsInfo()){
				AgentStatistic agentStatistic = new AgentStatistic();
				id2agentStatistic.put(info.getId(), agentStatistic);
				Stack<Integer> shorthestPath = GraphTools.getShorthestPath(graph, info.getStartVertexID(), info.getEndVertexID());
				if(shorthestPath == null){
					View.showMessage("Simulation can not be initialized.\nCould not find any path for agent with ID=" + info.getId(), MessageType.ERROR);
				}else{
					agentStatistic.minPath = shorthestPath.size();
				}
			}
			for(Integer o : params.getObservQueue()){
				if(o != null){
					agentManager.setObserv(true, o, o);
				}
				for(Integer p : params.getPredictQueue()){
					if(p != null){
						agentManager.setPredict(true, p, p);
					}
					for(Double r :params.getRelieQueue()){
						if(r != null){
							agentManager.setRelie(true, r);
						}
						for(int i =0;i<params.getRandOrderNr();i++){
							iteration = 0;
							agentManager.initializeAgentQueue(graph);
							if(params.isRandOrder()){
								agentManager.randomizeAgentOrder();
							}
							simulationStatistic = new SimulationStatistic();
							for(AgentStatistic s : id2agentStatistic.values()){
								s.reset();
							}
							
							while(iteration < PlatformManager.MAX_ITERATIONS && !agentManager.getAgentQueue().isEmpty()){
								Agent agent = agentManager.nextAgent();
								InternalAgentInfo info = agentManager.getAgentInfo(agent);
								AgentStatistic statistic = id2agentStatistic.get(info.getId());
								Integer observ = null;
								if(agentManager.isUseDefObserv()){
									observ = getNextRandom(agentManager.getDefaultMinObserv(), agentManager.getDefaultMaxObserv());
								}else{
									observ = getNextRandom(info.getMinObserv(), info.getMaxObserv());
								}
								Collection<AgentInfo> agentsInRange = graph.getAgentsInRange(info, agentManager.getQueuedAgentsInfo(), observ);
								agentsInRange.remove(info);
								int it = info.getIteration();
								if(it == iteration){
									iteration++;
								}
								info.setIteration(iteration);
								statistic.time -= System.nanoTime();
								Integer moveID = agent.getStep(agentsInRange);
								statistic.time += System.nanoTime();
								statistic.path = info.getIteration();
								if(moveID == null  ){
									agentManager.addAgent2Queue(agent);
								}else if(moveID.equals(info.getCurrentVertexID())){
									agentManager.addAgent2Queue(agent);										
								}else{
									Integer prevID = info.getCurrentVertexID();
									boolean a = canMove(prevID, moveID);
									double relie = info.getInternalRelie();
									if(agentManager.isUseDefReliab()){
										relie = agentManager.getDefaultReliab();
									}
									boolean b = relie >= rand.nextDouble();
									if(!b){
										statistic.failed++;
									}
									if( a&&b ){
										info.setCurrentVertexID(moveID);
									}					
									if(!info.isTargetAchived()){
										agentManager.addAgent2Queue(agent);					
									}else{
										info.setAgentState(AgentState.done);							
									}
								}
								Integer predict = null;
								if(agentManager.isUseDefPredict()){
									predict = getNextRandom(agentManager.getDefaultMinPredict(), agentManager.getDefaultMaxPredict());
								}else{
									predict = getNextRandom(info.getMinPredict(), info.getMaxPredict());
								}
								agentsInRange = graph.getAgentsInRange(info, agentManager.getQueuedAgentsInfo(), observ);
								agentsInRange.remove(info);
								statistic.time -= System.nanoTime();
								AgentPlan plan = agent.getPlan(predict,agentsInRange);
								statistic.time += System.nanoTime();
								info.setAgentPlan(plan);							
							}						
							
							
							SimulationStatistic statistic = getStatistic();
							statistic.observ = o;
							statistic.predict = p;
							statistic.relie =r;
							statistics.add(statistic);
							view.multirunProgress(++nr);	
						}
					}
				}
			}			
			restoreParams();
			view.multirunDone(statistics);
		}
		public LinkedList<SimulationStatistic> getStatistics(){
			return statistics;
		}
		public MultiRunParameters getMultiRunParameters(){
			return params;
		}
		
		private void backupParams(){
			 defaultReliab = agentManager.getDefaultReliab();
			defaultMinObserv = agentManager.getDefaultMinObserv();
			defaultMinPredict = agentManager.getDefaultMinPredict();
			defaultMaxObserv = agentManager.getDefaultMaxObserv();
			defaultMaxPredict = agentManager.getDefaultMaxPredict();
			useDefReliab = agentManager.isUseDefReliab();
			useDefObserv = agentManager.isUseDefObserv();
			useDefPredict = agentManager.isUseDefPredict();
		}
		private void restoreParams(){
			agentManager.setObserv(useDefObserv, defaultMinObserv, defaultMaxObserv);
			agentManager.setPredict(useDefPredict, defaultMinPredict, defaultMaxPredict);
			agentManager.setRelie(useDefReliab, defaultReliab);
		}
	
	}
	public void saveMultirunStats() {
		LinkedList<SimulationStatistic> statistics = multirunThread.getStatistics();
		String installDir = System.getProperty("installDir");
		String sep = System.getProperty("file.separator");
		StringBuilder builder = new StringBuilder();
		// TODO save multirun stats
		// save graph and sim too?
		// save in csv or for gnu plot (both) select two params
		// save name timestamp + size + some name + which param was changed?
		
		
		//PrintWriter out = new PrintWriter(installDir + sep + "results" + sep + filename);
	}

}
